﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using DotNetNuke.Common;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Entities.Users;
using DotNetNuke.Framework.Providers;
using DotNetNuke.NavigationControl;
using DotNetNuke.Services.Exceptions;
using DotNetNuke.UI.WebControls;

namespace BrandonHaynes.Providers.Navigation
	{
	public class TemplatedDNNMenuNavigationProvider : DNNMenuNavigationProvider
		{
		private const string providerPathAttributeName = "providerPath";
		private const string templateAttributeName = "templateName";
		private const string defaultTemplateControlName = "Default";
		private const string errorTemplateControlName = "Error.ascx";
		private const string identificationPattern = @"<\s*meta\s+name\s*=\s*""menuTemplate""\s+content\s*=\s*""(?<template>[^""]*?)""\s*/\s*>";
		private const string cssIdentificationPattern = @"<\s*meta\s+name\s*=\s*""menuCSS""\s+content\s*=\s*""(?<template>[^""]*?)""\s*/\s*>";

		private Dictionary<int, TabInfo> Tabs { get; set; }
		protected string ProviderTemplateName { get; set; }
		protected string ProviderPath { get; set; }

		public TemplatedDNNMenuNavigationProvider()
			: base()
			{
			var provider = ProviderConfiguration.GetProviderConfiguration("navigationControl").Providers.Values.Cast<Provider>().
				First(innerProvider => innerProvider.Type.StartsWith(this.GetType().FullName, StringComparison.OrdinalIgnoreCase));

			ProviderPath = provider.Attributes[providerPathAttributeName];
			ProviderTemplateName = provider.Attributes[templateAttributeName] != null ? ProviderPath + provider.Attributes[templateAttributeName] : null;
			Tabs = Extensions.GetTabMap();
			}

		public override void Bind(DNNNodeCollection objNodes)
			{
			// Binding is recursive, so we must determine if this is the "first" binding attempt
			// Otherwise we'll "do stuff" on every recursive call.  We only want to hook once.
			var isRootBinding = Menu.MenuNodes.IsEmpty();

			base.Bind(objNodes);

			// For the first binding, enumerate all nodes, check for templated-menu marker(s), and create
			if (isRootBinding)
				{
				var defaultTemplateName = Globals.ResolveUrl(
					SkinTemplateName ?? 
					ProviderTemplateName ??
					ProviderPath + defaultTemplateControlName);

				Menu
					.AsPreorderEnumerable()
					.Where(node => !node.MenuNodes.IsEmpty())
					.Where(node => Tabs.GetByNode(node).IsTemplatedMenu(identificationPattern))
					.ForEach(node => 
						{
							var underlyingTab = Tabs.GetByNode(node);
							CreateTemplatedMenu(node, underlyingTab.GetCustomTemplateName(identificationPattern, ProviderPath) ?? defaultTemplateName);
							CreateCssLink(underlyingTab.GetCustomTemplateName(cssIdentificationPattern, ProviderPath) ?? defaultTemplateName);
						});
				}
			}

		/// <summary>
		/// Creates a link element and adds it to the page header (if it's not already there)
		/// </summary>
		protected virtual void CreateCssLink(string cssName)
			{
			var id = Globals.CreateValidID(cssName);

			if (Menu.Page.Header.FindControl(id) == null)
				{
				var link = new HtmlLink();
				link.ID = id;
				link.Href = string.Concat(cssName, ".css");
				link.Attributes["rel"] = "stylesheet";
				link.Attributes["type"] = "text/css";

				Menu.Page.Header.Controls.Add(link);
				}
			}

		/// <summary>
		/// Given a root MenuNode, creates a templated menu using the given templateName
		/// </summary>
		protected virtual void CreateTemplatedMenu(MenuNode rootNode, string templateName)
			{
			var templatedHtml = GenerateTemplatedMenuHtml(templateName, rootNode);

			// Create a node that contains our rendered html
			var templatedNode = new MenuNode()
				{
					Text = templatedHtml,			// We use the fact that this menuing system supports arbitrary html content
					ClickAction = eClickAction.None	// to inject our generated html
				};

			// Remove the "default" menu hierarchy and inject our rendered html node
			rootNode.RemoveDescendants();
			rootNode.MenuNodes.Add(templatedNode);
			}

		/// <summary>
		/// Given a menutemplate and menu node, instantiates the template, databinds it,
		/// and generates HTML.  This HTML is injected into the menu hierarchy and the source
		/// nodes are removed.
		/// </summary>
		/// <param name="templateControl">A template control to databind against</param>
		/// <param name="rootNode">The root node to use in databinding</param>
		/// <returns>An HTML fragment of the templated results</returns>
		private string GenerateTemplatedMenuHtml(MenuTemplate templateControl, MenuNode rootNode)
			{
			using(var writer = new StringWriter(CultureInfo.CurrentCulture) { NewLine = string.Empty })
			using(var htmlWriter = new HtmlTextWriter(writer, string.Empty) { NewLine = string.Empty })
				{
				// Add the control to this collection so that databinding may obtain necessary context
				this.Controls.Add(templateControl);

				// Databind the template and render the html
				templateControl.DataSource = rootNode;
				templateControl.DataBind();
				templateControl.RenderControl(htmlWriter);

				// Return our html -- note that DNNMenu does not support newlines in the resultant html, so we strip
				return writer.ToString().Replace(System.Environment.NewLine, " ");
				}
			}

		/// <summary>
		/// Given the name of a template, loads that template, instantiates the template, databinds it,
		/// and generates HTML.  This HTML is injected into the menu hierarchy and the source
		/// nodes are removed.
		/// </summary>
		/// <param name="templateControlName">The name of a fully qualified template WITHOUT extension (.ascx is appended by this method)</param>
		/// <param name="rootNode">The root menu node used in templating</param>
		/// <returns>An HTML fragment representing the results of templating</returns>
		private string GenerateTemplatedMenuHtml(string templateControlName, MenuNode rootNode)
			{
			Control template;

			try
				{ template = LoadControl(string.Concat(templateControlName, ".ascx")); }
			catch (HttpException e) // Typically control not found or compile issue
				{
				Exceptions.LogException(e);

				if (UserController.GetCurrentUserInfo().IsSuperUser)
					throw;
				else
					template = LoadControl(ProviderPath + errorTemplateControlName);
				}

			return GenerateTemplatedMenuHtml((MenuTemplate)template, rootNode);
			}

		#region Private Properties

		private string SkinTemplateName
			{
			get
				{
				var customAttribute = CustomAttributes.SingleOrDefault(attribute => attribute.Name == templateAttributeName);
				return customAttribute != null ?
					PortalSettings.ActiveTab.SkinPath + customAttribute.Value :
					null;
				}
			}

		#endregion
		}
	}
